/*******************************************************************************
 * Copyright 2010 DemandSpot
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/
package com.demandspot.client.table;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import com.google.gwt.gen2.table.client.FixedWidthGrid;
import com.google.gwt.gen2.table.client.SortableGrid;
import com.google.gwt.gen2.table.client.TableModelHelper.ColumnSortList;
import com.google.gwt.gen2.table.event.client.ColumnSortEvent;
import com.google.gwt.user.client.ui.Widget;

public class DataTable<RowModel> extends FixedWidthGrid {
	private String[] rowsStyles = null;
	private RowFormatter formatter;
	private List<CellRenderer<RowModel, ?>> cellsRenders;
	private List<ColumnComparator<RowModel, ?>> comparators;
	private List<RowModel> dataModels,filterdModels,selectedRowsForSort;
	private Set<FilterRow<RowModel>> filters = new HashSet<FilterRow<RowModel>>();
	private int sortedColumn = -1;
	private boolean sortDirection = false;
	private DefaultColumnSorter sorter;
	
	@SuppressWarnings("unchecked")
	public DataTable(List<RowModel> dataModels, 
			List<CellRenderer<RowModel, ?>> cellsRenders, 
			String[] rowsStyles) {
		this.formatter = super.getRowFormatter();
		this.dataModels = dataModels;
		this.rowsStyles = rowsStyles;
		this.cellsRenders = cellsRenders;
		this.comparators = new ArrayList<ColumnComparator<RowModel,?>>();
		
		for (CellRenderer<RowModel, ?> renderer : this.cellsRenders) {
			this.comparators.add(new ColumnComparator(renderer));
		}
		
		sorter = new DefaultColumnSorter();
		this.setColumnSorter(sorter);
		
		this.resize(0, cellsRenders.size());
		buildTable();
	}
	
	public void buildTable() {
		List<RowModel> filterdModels = this.filterdModels != null ? new ArrayList<RowModel>(this.filterdModels) : null;
		List<RowModel> dataModels = new ArrayList<RowModel>(this.dataModels);
		
		resize(0, cellsRenders.size());
		clearAll();
		
		this.filterdModels = filterdModels;
		this.dataModels = dataModels;
		
		List<RowModel> models = getModelsToUse();
		
		for(int i=0; i < models.size(); i++) {
			insertModel(i, models.get(i));
		}
	}

	public void insertModel(int beforeRow, RowModel rowModel) {
		CellFormatter cellFormatter = getCellFormatter();
		
		this.insertRow(beforeRow);
		for(int j=0; j < cellsRenders.size(); j++) {
			CellRenderer<RowModel,?> cellRenderer = cellsRenders.get(j);
			Widget widgetToInsert = cellRenderer.getDisplayedWidget(rowModel);
			if (widgetToInsert != null) {
				this.setWidget(beforeRow, j, widgetToInsert);
			} else {
				String value = cellRenderer.getDisplayedString(rowModel);
				boolean isHTML = cellRenderer.isHTML();
				if (isHTML) {
					this.setHTML(beforeRow, j, value);
				} else {
					this.setText(beforeRow, j, value);
				}
			}
			cellFormatter.setHorizontalAlignment(beforeRow, j, cellRenderer.getHorizontalAlignment());
			if (cellRenderer.addRightBorder()) {
				cellFormatter.addStyleName(beforeRow, j, "right-border");
			}
			
			if (cellRenderer.addLeftBorder()) {
				cellFormatter.addStyleName(beforeRow, j, "left-border");
			}
			
		}
		fixStyles(beforeRow);
	}
	
	@SuppressWarnings("unchecked")
	private int findIndexForNewModel(RowModel rowModel, List<RowModel> models) {
		if (sortedColumn > -1) {
			Comparable comparable1 = cellsRenders.get(sortedColumn).getCompareValue(rowModel);
			for (int i=0; i < models.size(); i++) {
				RowModel rowModel2 = models.get(i);
				Comparable comparable2 = cellsRenders.get(sortedColumn).getCompareValue(rowModel2);
				
				int res = comparable1.compareTo(comparable2);
				if (sortDirection && res >=0 || !sortDirection && res <=0) {
					return res;
				}
			}
		}
		
		return models.size();
	}
	
	public List<RowModel> getSelectedModels(){
		List<RowModel> toReturn = new ArrayList<RowModel>();
		List<RowModel> models = getModelsToUse();
		Set<Integer> selected = getSelectedRows();
		for (Integer index : selected) {
			toReturn.add(models.get(index));
		}
		
		return toReturn;
	}
	
	public void setHTML(int row, int column, String html) {
		super.setHTML(row, column, html);
		setRowStyle(row);
	}

	public void setText(int row, int column, String text) {
		super.setText(row, column, text);
		setRowStyle(row);
	}

	public void setWidget(int row, int column, Widget widget) {
		super.setWidget(row, column, widget);
		setRowStyle(row);
	}

	public String[] getRowsStyles() {
		return rowsStyles;
	}

	public void setRowsStyles(String[] rowsStyles) {
		this.rowsStyles = rowsStyles;
		removeAllRowsStyles();
		formatAllRows();
	}

	private void formatAllRows() {
		int rowCount = super.getRowCount();
		removeAllRowsStyles();
		for(int i=0; i < rowCount; i++) {
			setRowStyle(i);
		}
	}
	
	public void sortColumn(int column, boolean ascending) {
		this.selectedRowsForSort = getSelectedModels();
		super.sortColumn(column, ascending);
//		formatAllRows();
	}
	
	public void addModel(RowModel rowModel) {
		if (rowModel != null) {
			int index = this.getRowCount();
			boolean modelRelevant = isModelRelevant(rowModel, filters);
			if (sortedColumn > -1) {
				index = findIndexForNewModel(rowModel, dataModels);
				dataModels.add(index, rowModel);
				
				if (filterdModels != null && modelRelevant) {
					index = findIndexForNewModel(rowModel, filterdModels);
					filterdModels.add(index, rowModel);
				}
			} else {
				dataModels.add(rowModel);
				if (filterdModels != null && modelRelevant) {
					filterdModels.add(rowModel);
				}
			}
			insertModel(index, rowModel);
		}
	}
	
	public List<RowModel> getAllModels() {
		return dataModels;
	}
	
	public List<RowModel> getFilterdModels() {
		return filterdModels;
	}
	
	public void removeFilter(FilterRow<RowModel> filter) {
		if (filterdModels != null && filters.remove(filter)) {
			if (filters.isEmpty()) {
				filterdModels = null;
				buildTable();
			} else {
				List<RowModel> removedModels = new ArrayList<RowModel>(dataModels);
				removedModels.removeAll(filterdModels);
				
				for (RowModel rowModel : removedModels) {
					if (isModelRelevant(rowModel, filters)) {
						dataModels.remove(rowModel);
						addModel(rowModel);
					}
				}
			}
		}
	}
	
	public void applyFilters(Set<FilterRow<RowModel>> filterRows, boolean replaceAll) {
		if (!replaceAll) {
			filterRows.removeAll(filters);
			filters.addAll(filterRows);
		} else {
			filterdModels = null;
			filters = filterRows; 
		}
		
		if (!filterRows.isEmpty()) {
			List<RowModel> models = getModelsToUse();
			List<RowModel> newFilterdModels = new ArrayList<RowModel>(models);
			List<RowModel> selected = getSelectedModels();
			for (Iterator<RowModel> iterator = newFilterdModels.iterator(); iterator.hasNext();) {
				RowModel rowModel = iterator.next();
				if (!isModelRelevant(rowModel, filterRows)) {
					int index = selected.indexOf(rowModel);
					if (index > -1) {
						this.deselectRow(index);
					}
					iterator.remove();
				}
			}
			filterdModels = newFilterdModels;
		} else if (filters.isEmpty()) {
			filterdModels = null;
		}
		buildTable();
	}
	
	@Override
	public void removeRow(int row) {
		super.removeRow(row);
		
		if (getModelsToUse().size() > row) {
			RowModel toRemove = null;
			if (filterdModels != null) {
				toRemove = filterdModels.remove(row);
			}
			
			if (toRemove != null) {
				dataModels.remove(toRemove);
			} else {
				dataModels.remove(row);
			}
			fixStyles(row);
		}
	}
	
	public void removeModel(RowModel rowModel) {
		List<RowModel> models = getModelsToUse();
		
		int index = models.indexOf(rowModel);
		
		if (index > -1) {
			removeRow(index);
		} else {
			dataModels.remove(rowModel);
		}
	}
	
	public void removeAllModels() {
		dataModels.clear();
		if (filterdModels != null) {
			filterdModels.clear();
		}
		if (filters != null) {
			filters.clear();
		}
		clearAll();
	}
	
	public void addModels(List<RowModel> models) {
		dataModels.addAll(models);
		buildTable();
	}
	
	private boolean isModelRelevant(RowModel rowModel, Set<FilterRow<RowModel>> filterRows) {
		if (filterRows != null) {
			for (FilterRow<RowModel> filter : filterRows) {
				if (!filter.doOperation(rowModel)) {
					return false;
				}
			}
		}
		return true;
	}
	
	private List<RowModel> getModelsToUse() {
		return filterdModels != null? filterdModels : dataModels;
	}
	
	private void fixStyles(int fromRow) {
		if (rowsStyles != null) {
			int rowCount = super.getRowCount();
			for(int i=fromRow; i < rowCount; i++) {
				for(int j=0; j < rowsStyles.length; j++) {
					formatter.removeStyleName(i, rowsStyles[j]);
				}
				setRowStyle(i);
			}
		}
	}
	
	private void removeAllRowsStyles() {
		if (rowsStyles != null) {
			int rowCount = super.getRowCount();
			for(int i=0; i < rowCount; i++) {
				for(int j=0; j < rowsStyles.length; j++) {
					formatter.removeStyleName(i, rowsStyles[j]);
				}
			}
		}
	}
	
	private void setRowStyle(int row) {
		if (rowsStyles != null) {
			String styleName = formatter.getStyleName(row);
			String rowStyleName = rowsStyles[row % rowsStyles.length];
			
			formatter.setStyleName(row, rowStyleName);
			if (styleName != null && !styleName.isEmpty()) {
				formatter.addStyleName(row, styleName);
			}
		}
	}
	
	public int getSortedColumn() {
		return sortedColumn;
	}

	public void setSortedColumn(int sortedColumn) {
		this.sortedColumn = sortedColumn;
	}

	public boolean isSortDirection() {
		return sortDirection;
	}

	public void setSortDirection(boolean sortDirection) {
		this.sortDirection = sortDirection;
	}



	private class DefaultColumnSorter extends ColumnSorter {
		@SuppressWarnings("deprecation")
		@Override
		public void onSortColumn(SortableGrid grid, ColumnSortList sortList, ColumnSorterCallback callback) {
			int column = sortList.getPrimaryColumn();
			boolean ascending = sortList.isPrimaryAscending();
			
			sortedColumn = column;
			sortDirection = ascending;
			
			sort(column, ascending);
			
			fireEvent(new ColumnSortEvent(sortList));
		}
		
		public void sort(int column, boolean ascending) {
			List<RowModel> selected = DataTable.this.selectedRowsForSort;
			ColumnComparator<RowModel, ?> comparator = comparators.get(column);	
			comparator.setAscending(ascending);
			
			List<RowModel> models = getModelsToUse();
			
			if (filterdModels != null) {
				Collections.sort(filterdModels, comparator);
			}
			Collections.sort(dataModels, comparator);
			DataTable.this.buildTable();
			models = getModelsToUse();
			
			for (RowModel rowModel : selected) {
				int index = models.indexOf(rowModel);
				DataTable.this.selectRow(index, false);
			}
		}
	}
}
